/**
 * The contents of this file are subject to the license and copyright detailed in the LICENSE and NOTICE files at the root of the source tree and available online at
 * http://www.askosi.org/license.html
 */
package be.destin.skos.core;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;

import org.apache.catalina.util.Base64;
import org.apache.log4j.Logger;

import be.destin.skos.html.SkosHtmlManager;

/**
 * @author Christophe Dupriez
 *
 */
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlType(propOrder={"baseURL","displayURLMatch","guidMatch","inKey","derivedScheme","outKeyMatch","guid","displayURL","previewURL","createURL","searchURL","atomURL","helpURL"})
//{"baseURL","displayURLMatch","guidMatch","inKey","outKeyMatch","guid","displayURL->display","previewURL","createURL->create","searchURL","atomURL","helpURL->help"}

public class Application extends Concept {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	/** log4j category */
	private static Logger log = Logger.getLogger(Application.class);
	
	protected String user;
	protected String password;
	protected String auth = "Basic";

/*
 * External (and internal) applications URL must be proxied for the "same origin" constraint of JavaScript (this is good for security reason also)
 * The proxy MUST be /proxy/applicationCode/rest of the URL after the baseURL...
 */
	protected String baseURL;		// root of URLs which is proxied
/*
 * The following URLs are related to specific items
 */
	/*
	 * Getting the ASKOSI compatible record id (letters, digits, underline) from received RSS
	 */
	protected String displayURLMatch;	// regexp to extract the record id FROM the application URL for a given record
	protected String guidMatch;		// regexp to extract the record id FROM the application RSS (guid element)
	protected String inKey="$1$2$3$4$5$6$7$8$9";	// replacement for this regexp to generate the record id (by default, concatenated elements)
	
	protected String derivedSchemeAbout; // About for a local scheme with entries identified with a Concept About equal to the record id.
	protected ConceptScheme derivedConceptScheme; // Corresponding accessible concept scheme

	/*
	 * Generating URLS from the ASKOSI compatible record id (letters, digits, underline)
	 */
	protected String outKeyMatch;	// regexp to extract the pieces of the record id
	protected String guid;			// replacement for outKeyMap regexp to generate the guid element
	protected String displayURL;	// replacement for outKeyMap regexp to generate the url
	protected String previewURL;	// Creates a preview (Internet Explorer Accelerators) for one record to display (outKeyMap)
/*
 * The following URLs are related to result sets and WILL follow OpenSearch like parameterization (should be fed from and OSDX!)
 */
	protected String createURL;		// page to create a record (OpenSearch parameters to specify a record "name" if known in advance)
	protected String searchURL;		// General search on a given term (OpenSearch parameters)
	protected String atomURL;		// Get a search result based on a given term (OpenSearch parameters) in RSS format
	protected String helpURL;		// page with a complete search form (OpenSearch parameters to specify some query terms)

    private Pattern inKeyPattern = null;
    private Pattern guidInKeyPattern = null;
    private Pattern outKeyPattern = null;

    /**
	 * @return the user
	 */
    @XmlAttribute
	public String getUser() {
		return user;
	}

	/**
	 * @param user the user to set
	 */
	public void setUser(String user) {
		this.user = user;
	}

	/**
	 * @return the password
	 */
    @XmlAttribute
	public String getPassword() {
		return password;
	}

	/**
	 * @return the password
	 */
	@XmlTransient
	public String getMethodToken() {
		String method = this.getAuth();
		if (method == null) return null;
		String token = getUser()+':';
		if ("Digest".equalsIgnoreCase(method)) {
			try {
				MessageDigest md = MessageDigest.getInstance("md5");
				md.update((getPassword().getBytes()));
				byte[] toks = token.getBytes();
				byte[] digest = md.digest();
				byte[] result = new byte[toks.length + digest.length]; 
				System.arraycopy(toks, 0, result, 0, toks.length); 
				System.arraycopy(digest, 0, result, toks.length, digest.length); 
				token = Base64.encode(result);
			} catch (NoSuchAlgorithmException e) {
				throw new UnsupportedOperationException("MD5 HASHING NOT AVAILABLE");
			}
		} else {
	        token = Base64.encode((token+getPassword()).getBytes());
		}
		return method + ' ' + token;
	}

	/**
	 * @param password the password to set
	 */
	public void setPassword(String password) {
		this.password = password;
	}

	/**
	 * @return the authentication method
	 */
    @XmlAttribute
	public String getAuth() {
		if (user == null || user.isEmpty()) return null;
		if (auth == null || auth.isEmpty()) return "Basic";
		return auth;
	}

	/**
	 * @param auth the authentication method to set (Basic, Digest)
	 */
	public void setAuth(String auth) {
		this.auth = auth;
	}

	/**
     * @return the local proxy URL (helper)
     */
    @XmlTransient
    public String getProxyRoot() {
    	return "/proxy/"+getAbout();
    }
    
    /**
	 * @return the inKeyPattern
	 */
	public Pattern getInKeyPattern() {
		return inKeyPattern;
	}

	/**
	 * @return the outKeyPattern
	 */
	public Pattern getOutKeyPattern() {
		return outKeyPattern;
	}

	/**
	 * @return the inKey
	 */
	public String getInKey() {
		return inKey;
	}

	/**
	 * @param inKey the inKey to set
	 */
	public void setInKey(String inKey) {
		this.inKey = inKey;
	}

	/**
	 * @return the derivedSchemeAbout
	 */
	public String getDerivedScheme() {
		return derivedSchemeAbout;
	}

	/**
	 * @param derivedSchemeAbout the derivedSchemeAbout to set
	 */
	public void setDerivedScheme(String derivedSchemeAbout) {
		this.derivedSchemeAbout = derivedSchemeAbout;
	}

	/**
	 * @return the derivedConceptScheme
	 */
	@XmlTransient
	public ConceptScheme getDerivedConceptScheme() {
		if (derivedConceptScheme != null) return derivedConceptScheme;
		if (derivedSchemeAbout != null && !derivedSchemeAbout.isEmpty()) {
			SchemeInterface thisScheme = this.getInScheme().getManagerInstance().getScheme(derivedSchemeAbout);
			if (thisScheme != null && thisScheme instanceof ConceptScheme) {
				derivedConceptScheme = (ConceptScheme)thisScheme;
			} else {
				log.error("ConceptScheme "+derivedSchemeAbout+" does not exist for application "+this.getAbout());
				derivedSchemeAbout = ""; // An empty derivedSchemeAbout indicates a MISSING scheme
			}
		}
		return derivedConceptScheme;
	}

	/**
	 * @param derivedConceptScheme the derivedConceptScheme to set
	 */
	public void setDerivedConceptScheme(ConceptScheme derivedConceptScheme) {
		this.derivedConceptScheme = derivedConceptScheme;
	}

	/*
	 * Returns the concept in the derived concept scheme with the key as a concept about.
	 */
	public Concept getDerivedConcept(String key) {
		ConceptScheme aScheme = this.getDerivedConceptScheme();
		if (aScheme == null) return null;
		return aScheme.getConcept(key);
	}

	/**
	 * @return the outKeyMatch
	 */
	public String getOutKeyMatch() {
		return outKeyMatch;
	}

	/**
	 * @param outKeyMatch the outKeyMatch to set
	 */
	public void setOutKeyMatch(String outKeyMatch) {
		this.outKeyMatch = outKeyMatch;
		this.outKeyPattern = null;
		if (this.outKeyMatch != null && !this.outKeyMatch.isEmpty()) { 
			try {
				this.outKeyPattern = Pattern.compile(this.outKeyMatch);
			} catch (PatternSyntaxException e) {
				String message = this.getAbout()+", outKeyMatch regular expression \""+this.outKeyMatch+"\" is invalid: " +e.toString();
				log.error(message);
				this.getErrors().add(message);
			}
		}
	}

	/**
	 * @return the displayURLMatch
	 */
	public String getDisplayURLMatch() {
		return displayURLMatch;
	}

	/**
	 * @param displayURLMatch the displayURLMatch to set
	 */
	public void setDisplayURLMatch(String displayURLMatch) {
		this.displayURLMatch = displayURLMatch;
		this.inKeyPattern = null;
		if (this.displayURLMatch != null && !this.displayURLMatch.isEmpty()) { 
			try {
				this.inKeyPattern = Pattern.compile(this.displayURLMatch);
			} catch (PatternSyntaxException e) {
				String message = this.getAbout()+", displayURLMatch regular expression \""+this.displayURLMatch+"\" is invalid: " +e.toString();
				log.error(message);
				this.getErrors().add(message);
			}
		}
		// By default, both matches are equivalent
		if (this.guidMatch == null) guidMatch = displayURLMatch;
	}

	/**
	 * @return the displayURL
	 */
	public String getDisplayURL() {
		return displayURL;
	}

	/**
	 * @param displayURL the displayURL to set
	 */
	public void setDisplayURL(String displayURL) {
		this.displayURL = displayURL;
	}

	/**
	 * @return the previewURL
	 */
	public String getPreviewURL() {
		return previewURL;
	}

	/**
	 * @param previewURL the previewURL to set
	 */
	public void setPreviewURL(String previewURL) {
		this.previewURL = previewURL;
	}

	/**
	 * @return the helpURL
	 */
	public String getHelpURL() {
		return helpURL;
	}

	/**
	 * @param helpURL the helpURL to set
	 */
	public void setHelpURL(String helpURL) {
		this.helpURL = helpURL;
	}

	/**
	 * @return the createURL
	 */
	public String getCreateURL() {
		return createURL;
	}

	/**
	 * @param createURL the createURL to set
	 */
	public void setCreateURL(String createURL) {
		this.createURL = createURL;
	}

	/**
	 * @return the searchURL
	 */
	public String getSearchURL() {
/*
		if (searchURL == null || searchURL.isEmpty()) {
			ConceptScheme aScheme = getDerivedConceptScheme();
			if (aScheme == null) return null;
			else {
				SkosManager skosManager = aScheme.getManagerInstance();
				searchURL = ((Application)skosManager.getApplications().getConcept(SkosHtmlManager.askosiDisplayApplication)).getSearchURL();
				//TODO: AJOUTER le Scheme
			}
		}
*/
		return searchURL;
	}

	/**
	 * @param searchURL the searchURL to set
	 */
	public void setSearchURL(String searchURL) {
		this.searchURL = searchURL;
	}

	/**
	 * @return the atomURL
	 */
	public String getAtomURL() {
		return atomURL;
	}

	/**
	 * @param atomURL the atomURL to set
	 */
	public void setAtomURL(String atomURL) {
		this.atomURL = atomURL;
	}

	public boolean equals(Object o) {
		return super.equals(o);
	}

	public String getInKey(String data) {
		if (inKeyPattern == null) return data;
		try {
			Matcher match = inKeyPattern.matcher(data);
			if (match.find()) {
				return inKeyPattern.matcher(match.group(0)).replaceFirst(inKey);
			} else {
				log.info('"'+this.displayURLMatch+"\" not found in \""+data+"\"");
				return data;
			}
		} catch (Exception e) {
			return data;
		}
	}

	public String getGuidInKey(String data) {
		if (guidInKeyPattern == null) return data;
		try {
			Matcher match = guidInKeyPattern.matcher(data);
			if (match.find()) {
				return guidInKeyPattern.matcher(match.group(0)).replaceFirst(inKey);
			} else {
				log.info('"'+this.guidMatch+"\" not found in \""+data+"\"");
				return data;
			}
		} catch (Exception e) {
			return data;
		}
	}

	public String getGuid(String record) {
		if (guid == null || guid.isEmpty()) return record;
		if (outKeyPattern == null) return guid+record;
		try {
			Matcher match = outKeyPattern.matcher(record);
			if (match.find()) {
				return outKeyPattern.matcher(match.group(0)).replaceFirst(guid);
			} else {
				log.info('"'+this.outKeyMatch+"\" not found in \""+record+"\"");
				return record;
			}
		} catch (Exception e) {
			return record;
		}
	}

	public String getDisplayURL(String record) {
		if (displayURL == null || displayURL.isEmpty()) {
			Concept aConcept = getDerivedConcept(record);
			if (aConcept == null) return record;
			else {
				ConceptScheme aScheme = aConcept.getInScheme();
				SkosManager skosManager = aScheme.getManagerInstance();
				return ((Application)skosManager.getApplications().getConcept(SkosHtmlManager.askosiDisplayApplication)).getDisplayURL(aConcept.getScheme_About());
			}
		}
		if (outKeyPattern == null) return displayURL+record;
		try {
			Matcher match = outKeyPattern.matcher(record);
			if (match.find()) {
				return outKeyPattern.matcher(match.group(0)).replaceFirst(displayURL);
			} else {
				log.info('"'+this.outKeyMatch+"\" not found in \""+record+"\"");
				return record;
			}
		} catch (Exception e) {
			return record;
		}
	}

	public String getPreviewURL(String record) {
		if (previewURL == null || previewURL.isEmpty()) {
			Concept aConcept = getDerivedConcept(record);
			if (aConcept == null) return record;
			else {
				ConceptScheme aScheme = aConcept.getInScheme();
				SkosManager skosManager = aScheme.getManagerInstance();
				return ((Application)skosManager.getApplications().getConcept(SkosHtmlManager.askosiDisplayApplication)).getPreviewURL(aConcept.getScheme_About());
			}
		}
		if (outKeyPattern == null) return previewURL+record;
		try {
			Matcher match = outKeyPattern.matcher(record);
			if (match.find()) {
				return outKeyPattern.matcher(match.group(0)).replaceFirst(previewURL);
			} else {
				log.info('"'+this.outKeyMatch+"\" not found in \""+record+"\"");
				return record;
			}
		} catch (Exception e) {
			return record;
		}
	}

	/**
	 * @return the baseURL
	 */
	public String getBaseURL() {
		if (baseURL == null || baseURL.isEmpty()) return getUrl(TermList.anyLanguage);
		return baseURL;
	}

	/**
	 * @param baseURL the baseURL to set
	 */
	public void setBaseURL(String baseURL) {
		this.baseURL = baseURL;
	}

	/**
	 * @return the guidMatch
	 */
	public String getGuidMatch() {
		return guidMatch;
	}

	/**
	 * @param guidMatch the guidMatch to set
	 */
	public void setGuidMatch(String guidMatch) {
		this.guidMatch = guidMatch;
		this.guidInKeyPattern = null;
		if (this.guidMatch != null && !this.guidMatch.isEmpty()) { 
			try {
				this.guidInKeyPattern = Pattern.compile(this.guidMatch);
			} catch (PatternSyntaxException e) {
				String message = this.getAbout()+", guidMatch regular expression \""+this.guidMatch+"\" is invalid: " +e.toString();
				log.error(message);
				this.getErrors().add(message);
			}
			// By default, both matches are equivalent
			if (this.displayURLMatch == null) displayURLMatch = guidMatch;
		}
	}

	/**
	 * @return the guid
	 */
	public String getGuid() {
		return guid;
	}

	/**
	 * @param guid the guid to set
	 */
	public void setGuid(String guid) {
		this.guid = guid;
	}

	private class BaseFilenameFilter implements FilenameFilter {
		String application;
		
		public void setApplication(String appName) {
			application = appName;
		}

	    public boolean accept(File dir, String name) {
	        return name.startsWith(application+"2");
	    }
	}

	public synchronized void loadUsages() {
		SkosManager skosManager = this.getInScheme().getManagerInstance();
		File dir = new File(skosManager.getConfigDirectory()+File.separator+"usage"); 
		// It is also possible to filter the list of returned files.
		// This example does not return any files that start with `.'.
		BaseFilenameFilter filter = new BaseFilenameFilter();
		filter.setApplication(this.getAbout());
		String latestFile = null;
		try {
			String[] children = dir.list(filter);
			if (children != null && children.length > 0 ) {
				for (String aName : children) {
					if (latestFile == null) latestFile = aName;
					else if (aName.compareToIgnoreCase(latestFile) > 0) latestFile = aName;
				}
				if (latestFile != null) {
					//LOAD THE FILE CONTENT
					File cacheFile = new File(dir,latestFile);
					BufferedReader cacheReader = null;
					try {
						cacheReader = new BufferedReader(new FileReader(cacheFile));
						String strLine;
						while ((strLine = cacheReader.readLine()) != null)   {
							String[] fields = strLine.split("\t");
							if (fields == null || fields.length < 3) {
								log.error("line too short (Concept tab Role tab Count are requested) in "+cacheFile.getPath());
								break; // too few fields: BAD FILE!
							}
							int count = 0;
							try {
								count = Integer.parseInt(fields[2]);
							} catch (NumberFormatException e) {
								log.error("invalid count \""+fields[2]+"\" in "+cacheFile.getPath());
								break; // BAD NUMBER
							}
							String role = fields[1];
							Concept aRole = skosManager.getRoles().getConcept(role);
							if (aRole == null) {
								log.info("Role "+role+" is in "+cacheFile.getPath()+" but not in the list of defined roles. Line ignored.");
								continue;
							}
							Concept aConcept = skosManager.getScheme_Concept(fields[0]);
							if (aConcept == null) {
								log.info("Concept "+fields[0]+" is in "+cacheFile.getPath()+" but not in the known Concepts. Line ignored.");
								continue;
							}
							aConcept.putApplicationUsage(this.getAbout(), role, count, null, true);
							ConceptScheme aScheme = aConcept.getInScheme();
							if (aScheme != null) {
								SchemeUsage su = aScheme.getApplicationUsage(this.getAbout(), role);
								if (su == null) {
									su = new SchemeUsage(aConcept.getInScheme(),this.getAbout(), role);
									aScheme.putSchemeUsage(su);
								}
							}
						}
						//Close the input stream
						cacheReader.close();
					} catch (Exception e){//Catch exception if any
						log.error(cacheFile.getPath() + ": " + e.getMessage());
						if (cacheReader != null)
							try {
								cacheReader.close();
							} catch (IOException e1) {
								// TODO Auto-generated catch block
								e1.printStackTrace();
							}
					}
				}
			}
		} finally { }

	}
}
